﻿using System;
using System.Collections.Generic;
using System.IO;

namespace NBuild.Fx
{
    public sealed class FileTaskItems : TaskItems<FileInfo>
    {
        private readonly List<FileSpec> _includeFiles = new List<FileSpec>();
        private readonly List<FileSpec> _excludeFiles = new List<FileSpec>();
        private readonly UniqueCollection<string> _excludeDirectories = new UniqueCollection<string>(StringComparer.InvariantCultureIgnoreCase);

        public void ExcludeDirectory(string directory)
        {
            _excludeDirectories.Add(directory);
        }

        public void ExcludeFiles(string directory, string fileMask, bool includeSubdirectories)
        {
            if (_excludeDirectories.Contains(dir => directory.EndsWith(dir, StringComparison.InvariantCultureIgnoreCase)))
                return;

            if (fileMask == "*.*" && includeSubdirectories)
                ExcludeDirectory(directory);
            else
                _excludeFiles.Add(new FileSpec(directory, fileMask, includeSubdirectories));
        }

        public void IncludeFiles(string directory, string fileMask, bool includeSubdirectories)
        {
            if (!_excludeDirectories.Contains(dir => directory.EndsWith(dir, StringComparison.InvariantCultureIgnoreCase)))
                _includeFiles.Add(new FileSpec(directory, fileMask, includeSubdirectories));
        }

        public override IEnumerator<FileInfo> GetEnumerator()
        {
            for (int fileIdx = _excludeFiles.Count - 1; fileIdx >= 0; fileIdx--)
                if (_excludeDirectories.Contains(_excludeFiles[fileIdx].Directory))
                    _excludeFiles.RemoveAt(fileIdx);

            var excludeFiles = new UniqueCollection<FileInfo>(new FileInfoComparer());
            foreach (FileSpec fileSpec in _excludeFiles)
                BuildFileList(excludeFiles, fileSpec);

            for (int fileIdx = _includeFiles.Count - 1; fileIdx >= 0; fileIdx--)
                if (_excludeDirectories.Contains(_includeFiles[fileIdx].Directory))
                    _includeFiles.RemoveAt(fileIdx);

            var includeFiles = new UniqueCollection<FileInfo>(new FileInfoComparer());
            foreach (FileSpec fileSpec in _includeFiles)
                BuildFileList(includeFiles, fileSpec);

            foreach (FileInfo file in includeFiles)
            {
                if (FileInExcludeDirectories(file))
                    continue;
                if (excludeFiles.Contains(file))
                    continue;
                yield return file;
            }
        }

        private bool FileInExcludeDirectories(FileInfo file)
        {
            foreach (string excludeDirectory in _excludeDirectories)
                if (file.FullName.StartsWith(excludeDirectory, StringComparison.InvariantCultureIgnoreCase))
                    return true;
            return false;
        }

        private static void BuildFileList(UniqueCollection<FileInfo> fileList, FileSpec fileSpec)
        {
            var directory = new DirectoryInfo(fileSpec.Directory);
            fileList.AddRange(directory.GetFiles(fileSpec.FileMask));
            
            if (!fileSpec.IncludeSubdirectories)
                return;

            DirectoryInfo[] subdirectories = directory.GetDirectories();
            foreach (DirectoryInfo subdirectory in subdirectories)
                BuildFileList(fileList, subdirectory, fileSpec.FileMask);
        }

        private static void BuildFileList(UniqueCollection<FileInfo> fileList, DirectoryInfo directory, string fileMask)
        {
            fileList.AddRange(directory.GetFiles(fileMask));

            DirectoryInfo[] subdirectories = directory.GetDirectories();
            foreach (DirectoryInfo subdirectory in subdirectories)
                BuildFileList(fileList, subdirectory, fileMask);
        }

        #region FileSpec class

        private sealed class FileSpec
        {
            internal readonly string Directory;
            internal readonly string FileMask;
            internal readonly bool IncludeSubdirectories;

            internal FileSpec(string directory, string fileMask, bool includeSubdirectories)
            {
                Directory = directory;
                FileMask = fileMask;
                IncludeSubdirectories = includeSubdirectories;
            }
        }

        #endregion

        #region FileInfo comparer class

        private sealed class FileInfoComparer : IComparer<FileInfo>
        {
            int IComparer<FileInfo>.Compare(FileInfo x, FileInfo y)
            {
                return string.Compare(x.FullName, y.FullName, StringComparison.InvariantCultureIgnoreCase);
            }
        }

        #endregion
    }
}
